// Simple PIO-based (non-DMA) IDE driver code.

// TODO: include any files you need
#include <inc/assert.h>
#include <inc/traps.h>
#include <inc/x86.h>

#include <kern/spinlock.h>
#include <kern/sleeplock.h>
#include <kern/cpu.h>
#include <kern/buf.h>
#include <kern/proc.h>

#define SECTOR_SIZE 512

#define IDE_BSY 0x80
#define IDE_DRDY 0x40
#define IDE_DF 0x20
#define IDE_ERR 0x01

#define IDE_CMD_READ 0x20
#define IDE_CMD_WRITE 0x30
#define IDE_CMD_RDMUL 0xc4
#define IDE_CMD_WRMUL 0xc5

// You must hold idelock while manipulating queue.
static struct spinlock idelock;
static struct buf *idequeue;

static int havedisk1;

/* Busy Wait for IDE disk to become ready.
 * todo:
 * poll the status bits(status register) until the busy bit is clear and the ready bit is set.
 * check if IDE disk is error when checkkerr is set.
 * return value:
 * if checkerr is set: return -1 when IDE disk is error, return 0 otherwise.
 * else return 0 all the time.
 * Hint:
 * 1. you may use inb or insl to get data from I/O Port.
 * 2. Infomation about the status register can be found in ATA_PIO_Mode on https://osdev.org
 * 3. here are some MARCO you may need: IDE_* (IDE_CMD_* is not included)
 */
static int idewait(int checkerr)
{
	int temp;

	// read status reg
	while(((temp = inb(0x1f7)) & (IDE_BSY|IDE_DRDY)) != IDE_DRDY)
		;
	if(checkerr && (temp & (IDE_DF|IDE_ERR)) != 0)
		return -1;
	else
		return 0;
}

/* Init IDE disk driver, and check if you have disk 1.
 * todo: 
 * wait for IDE disk ready
 * Switch to disk 1 (use Drive/Head Register)
 * Check if disk 1 is present (poll Status Register for 1000 times, see if it responds)
 * Set havedisk1 if disk 1 is present
 * Switch back to disk 0
 * Hint:
 * 1. you may use outb or outsl to send data to I/O Port.
 * 2. Infomation about Drive/Head Register and Status Register can be found in ATA_PIO_Mode on https://osdev.org
 */
void ideinit(void)
{
    //init the lock and pic first.
    spin_initlock(&idelock);
    ioapicenable(IRQ_IDE, ncpu - 1);

    //todo: your code here.
	idewait(0);

	// select drive
	outb(0x1f6, 0xe0 | (1 << 4));
	for (int i = 0; i < 1000; i++)
		if (0 != inb(0x1f7)) {
			havedisk1 = 1;
			break;
		}

	// switch back to disk 0
	outb(0x1f6, 0xe0);
}

/* Start the request for b. Caller must hold idelock.
 * todo:
 * start a read/write request for buf b on disk device correctly according to buf b
 * Hint:
 * 1. Information can be found in ATA_PIO_Mode and ATA_Command_Matrix on https://osdev.org
 * 2. here are some MARCO you may need: IDE_CMD_* SECTOR_SIZE and BSIZE(define in fs.h) 
 * 3. notice the difference between Block size and Sector size
 * 4. write the data here if you need, but read the data when you get the interrupt(not here).
 * 5. inb insl outb and outsl might be used
 */
static void idestart(struct buf *b)
{
	//todo: your code here.
	int blk_sz = BSIZE / SECTOR_SIZE;
	int sector = b->blockno * blk_sz;
	int read_cmd = (blk_sz == 1) ? IDE_CMD_READ :	IDE_CMD_RDMUL;
	int write_cmd = (blk_sz == 1) ? IDE_CMD_WRITE : IDE_CMD_WRMUL;

	idewait(0);

	outb(0x3f6, 0);	// enable interrupt

	outb(0x1f2, blk_sz); // sector count
	outb(0x1f3, sector & 0xff); // LBAlo sector number
	outb(0x1f4, (sector >> 8) & 0xff); // LBAmid cylinder low
	outb(0x1f5, (sector >> 16) & 0xff); // LBAhi cylinder high
	outb(0x1f6, 0xe0 | ((b->dev & 1) << 4) | ((sector >> 24) & 0x0f)); // select drive

	if(b->flags & B_DIRTY){
		outb(0x1f7, write_cmd); // command
		outsl(0x1f0, b->data, BSIZE / 4);
	} else {
		outb(0x1f7, read_cmd);
	}
}

/* Interrupt handler.
 * todo:
 * IMPORTANT: get the idelock at first!
 * read data here if you need
 * update the buf's flag
 * wake up the process waiting for this buf
 * start the right ide request
 * IMPORTANT: release the idelock!
 * Hint:
 * 1. you may use idequeue to manage all the ide request
 * 2. in challenge problem, you should start the right ide request according to your algorithm, while the next ide request in normal problem.
 */
void ideintr(void)
{
	struct buf *b;

	spin_lock(&idelock);

	b = idequeue;
	if (NULL == b) {
		spin_unlock(&idelock);
		return;
	}
	idequeue = b->next;

	if (!(b->flags & B_DIRTY) && idewait(1) == 0)
		insl(0x1f0, b->data, BSIZE / 4);

	b->flags = (b->flags | B_VALID) & ~B_DIRTY;
	wakeup(b);

	if (NULL != idequeue)
		idestart(idequeue);

	spin_unlock(&idelock);
}

/* Sync buf b with disk.(in another word:ide request)
 * If B_DIRTY is set, write buf to disk, clear B_DIRTY, set B_VALID.
 * Else if B_VALID is not set, read buf from disk, set B_VALID.
 * todo:
 * IMPORTANT: get the idelock at first!
 * append b to idequeue
 * start this request if idequeue is empty before
 * wait for ide request to finish sleeplock may be suitable.
 * IMPORTANT:Release the lock
 * Hint:
 * 1.busy wait or spin-lock may be not suitable,you may need implement sleeplock
 */
void iderw(struct buf *b)
{
	struct buf **buf_ptr;

	if (!sleep_holding(&b->lock))
		panic("iderw: buf not locked");
	if ((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
		panic("iderw: nothing to do");
	if(b->dev != 0 && !havedisk1)
		panic("iderw: ide disk 1 not present");

	spin_lock(&idelock);

	// inqueue
	b->next = NULL;
	for(buf_ptr = &idequeue; *buf_ptr; buf_ptr=&(*buf_ptr)->next)
		;
	*buf_ptr = b;

	if (idequeue == b)
		idestart(b);

	while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){
		sleep(b, &idelock);
	}

	spin_unlock(&idelock);
}
